Skip to content

feat(scanner): index projects rooted at a symlink#258

Open
kryptt wants to merge 1 commit into
yoanbernabeu:mainfrom
kryptt:pr/symlink-root
Open

feat(scanner): index projects rooted at a symlink#258
kryptt wants to merge 1 commit into
yoanbernabeu:mainfrom
kryptt:pr/symlink-root

Conversation

@kryptt

@kryptt kryptt commented May 28, 2026

Copy link
Copy Markdown

Problem

`filepath.WalkDir` does not descend into symlinked directories, so when a user points grepai at a project root that is itself a symlink, the scan silently enumerates zero files. There's no warning — just no results.

This is hostile to a common pattern: fronting a version-managed path with a stable name so the workspace survives upstream version bumps. For example:

```
~/.emacs.d/emacs-sources -> /usr/share/emacs/30.2/
```

A user puts `emacs-sources` in their workspace config so they don't have to retarget on every emacs upgrade. Today, that workspace indexes nothing.

Solution

`NewScanner` now `os.Lstat`s the root and, only if it is a symlink to a directory, resolves it once via `filepath.EvalSymlinks` before `WalkDir` runs. Intermediate symlinks deeper in the tree are still skipped — that matches upstream's existing behavior and avoids recursion loops or surprising blow-ups when packages vendor third-party trees via symlinks.

We deliberately do not call `EvalSymlinks` on a real directory: doing so would also rewrite parent-directory symlinks (e.g. `/usr/local` on a Nix system) and silently shift the project's stored path in ways the caller never asked for. The `os.Lstat` guard makes the resolution opt-in based on the root entry's own type.

Test plan

Two new tests in `indexer/scanner_test.go`:

Test Asserts
`TestScanner_SymlinkProjectRoot` a symlink-to-directory root enumerates the underlying files; relative paths returned are relative to the resolved real root (not the symlink path)
`TestScanner_RealDirRoot_NotEvalSymlinks` when the root is a real directory (even if its parent is reached through a symlink), `scanner.root` stays as the caller-provided path, no silent rewrite

Both tests `t.Skipf` cleanly on environments that cannot create symlinks. All existing tests continue to pass (`go test ./...`).

filepath.WalkDir does not descend into symlinked directories, so a
project rooted at a symlink would silently enumerate zero files —
hostile to common patterns like

  ~/.emacs.d/emacs-sources -> /usr/share/emacs/30.2/

where a stable name fronts a versioned path so the workspace survives
upstream version bumps.

NewScanner now Lstats the root and, only if it is a symlink, resolves
it once via filepath.EvalSymlinks before WalkDir runs. Intermediate
symlinks deeper in the tree are still ignored, matching upstream's
prior caution against recursion loops.

We deliberately do NOT call EvalSymlinks on a real directory: doing
so would rewrite parent-directory symlinks (e.g. /usr/local on a Nix
system) and shift the project's stored path in ways the caller never
asked for. Two tests cover both cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant